#include "enumconverter_p.h"
#include "QtJsonSerializer/exception.h"
#include "QtJsonSerializer/cborserializer.h"
#include "QtJsonSerializer/qtjsonserializer_global.h"
#include <QByteArray>
#include <QtCore/qmetatype.h>

#include <cmath>
using namespace QtJsonSerializer;
using namespace QtJsonSerializer::TypeConverters;

namespace {

	Q_NORETURN inline void throwSer(const QByteArray& what, bool ser)
	{
		if (ser)
			throw SerializationException{ what };
		else
			throw DeserializationException{ what };
	}

}

EnumConverter::EnumConverter()
{
	setPriority(TypeConverter::Low);
}

bool EnumConverter::canConvert(int metaTypeId) const
{
	return QMetaType(metaTypeId).flags().testFlag(QMetaType::IsEnumeration) ||
		testForEnum(metaTypeId);  // NOTE check once in a while if still needed
}

QList<QCborTag> EnumConverter::allowedCborTags(int metaTypeId) const
{
	const auto metaEnum = getEnum(metaTypeId, false);
	if (metaEnum.isFlag())
		return { static_cast<QCborTag>(CborSerializer::Flags) };
	else
		return { static_cast<QCborTag>(CborSerializer::Enum) };
}

QList<QCborValue::Type> EnumConverter::allowedCborTypes(int metaTypeId, QCborTag tag) const
{
	Q_UNUSED(metaTypeId)
		Q_UNUSED(tag)
		return { QCborValue::Integer, QCborValue::String };
}

QCborValue EnumConverter::serialize(int propertyType, const QVariant& value) const
{
	const auto metaEnum = getEnum(propertyType, true);
	const auto tag = static_cast<QCborTag>(metaEnum.isFlag() ? CborSerializer::Flags : CborSerializer::Enum);
	if (helper()->getProperty("enumAsString").toBool()) {
		if (metaEnum.isFlag())
			return { tag, QString::fromUtf8(metaEnum.valueToKeys(value.toInt())) };
		else
			return { tag, QString::fromUtf8(metaEnum.valueToKey(value.toInt())) };
	}
	else
		return { tag, value.toInt() };
}

QVariant EnumConverter::deserializeCbor(int propertyType, const QCborValue& value, QObject* parent) const
{
	Q_UNUSED(parent)
		const auto metaEnum = getEnum(propertyType, false);
	auto cValue = value.isTag() ? value.taggedValue() : value;
	if (cValue.isString()) {
		auto result = -1;
		auto ok = false;
		if (metaEnum.isFlag())
			result = metaEnum.keysToValue(qUtf8Printable(cValue.toString()), &ok);
		else
			result = metaEnum.keyToValue(qUtf8Printable(cValue.toString()), &ok);
		if (ok)
			return result;
		else if (metaEnum.isFlag() && cValue.toString().isEmpty())
			return 0;
		else {
			throw DeserializationException{ QByteArray{"Invalid value for enum type \""} +
												metaEnum.name() +
												"\": " +
												cValue.toString().toUtf8() };
		}
	}
	else {
		const auto intValue = cValue.toInteger();
		if (!metaEnum.isFlag() && metaEnum.valueToKey(intValue) == nullptr) {
			throw DeserializationException{ "Invalid integer value. Not a valid enum/flags element: " +
												QByteArray::number(intValue) };
		}
		return static_cast<int>(intValue);
	}
}

QVariant EnumConverter::deserializeJson(int propertyType, const QCborValue& value, QObject* parent) const
{
	if (value.isDouble()) {
		double intpart;
		if (std::modf(value.toDouble(), &intpart) != 0.0) {
			throw DeserializationException{ "Invalid value (double) for enum type found: " +
												QByteArray::number(value.toDouble()) };
		}
	}
	return deserializeCbor(propertyType, value, parent);
}

bool EnumConverter::testForEnum(int metaTypeId) const
{
	try {
		getEnum(metaTypeId, true);
		return true;
	}
	catch (Exception&) {
		return false;
	}
}

QMetaEnum EnumConverter::getEnum(int metaTypeId, bool ser) const
{
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
	const auto mo = QMetaType::metaObjectForType(metaTypeId);
#else
	const auto mo = QMetaType(metaTypeId).metaObject();
#endif
	if (!mo)
	{
		const QByteArray hexData = QByteArray("Unable to get metaobject for type ") + QMetaTypeName(metaTypeId);
		throwSer(hexData, ser);
	}
	const auto enumName = QString::fromUtf8(QMetaTypeName(metaTypeId))
		.split(QStringLiteral("::"))
		.last()
		.toUtf8();
	auto mIndex = mo->indexOfEnumerator(enumName.data());
	if (mIndex < 0) {
		const QByteArray hexData = QByteArray{ "Unable to get QMetaEnum for type " } + QMetaTypeName(metaTypeId) + QByteArray{ " using the owning meta object " } + mo->className();
		throwSer(QByteArray("Unable to get QMetaEnum for type "), ser);
	}

	return mo->enumerator(mIndex);
}
